<?php
/**
 * Разбор WSDL на типы данных и методы
 * 
 * PHP version 5
 * 
 * @category Framework
 * @package  Core
 * @author   Andrey Filippov <afi@i-loto.ru>
 * @license  %license% name
 * @version  SVN: $Id: new.php 9 2007-12-25 11:26:03Z afi $
 * @link     https://solo-framework-lib.googlecode.com
 */


class WSDLParser
{
	
	/**
	 * Path or URL to WSDL file
	 * 
	 * @var string
	 */
	private $wsdlFile = null;
	
	/**
	 * DOMDocument contents WSDL
	 * 
	 * @var DOMDocument
	 */
	private $dom = null;

	/**
	 * 
	 * @var DOMXPath
	 */
	private $xpath = null;
	
	/**
	 * Collection of data types
	 * 
	 * @var mixed
	 */
	private $collector = array();
	
	/**
	 * Collection of WSDL methods
	 * 
	 * @var array
	 */
	private $methods = null;
	
	/**
	 * Имя элемента, содержащего набор операций веб-сервиса
	 * 
	 * @var string
	 */
	private $portType = null;
	
	/**
	 * Список названий примитивных типов
	 * 
	 * @var array
	 */
	private $primitiveTypes = array();
	
	/**
	 * Список типов
	 * 
	 * @var array
	 */
	private $rawTypesMap = null;
	
	/**
	 * Целевое пространство имен веб-сервиса
	 * 
	 * @var string
	 */
	public $targetNamespace = null;
	
	/**
	 * Включение\выключение лога
	 * 
	 * @var boolean
	 */
	public $logEnabled = true;

	/**
	 * Возвращает типы данных
	 * 
	 * @return mixed
	 */
	public function getDataTypes()
	{
		return $this->collector;
	}

	/**
	 * Возвращает методы веб-сервиса
	 * 
	 * @return array
	 */
	public function getMethods()
	{
		return $this->methods;
	}
	/**
	 * Конструктор
	 * 
	 * @param string $wsdlFile Путь к WSDL
	 * @param string $portType Имя элемента, содержащего набор операций веб-сервиса
	 * @param array $primitiveTypes Список названий примитивных типов
	 * 
	 * @return void
	 */
	public function __construct($wsdlFile, $portType = null, $primitiveTypes, $logEnabled = true)
	{
		$this->log("Start WSDL2PHP tool", true);
		
		$this->wsdlFile = $wsdlFile;
		$this->portType = $portType;
		$this->primitiveTypes = $primitiveTypes;
		$this->logEnabled = $logEnabled;
		
	
		$this->log("Loading WSDL from {$this->wsdlFile}");
		$this->dom = new DOMDocument();
		$res = @$this->dom->load($this->wsdlFile);
		if (!$res)
			throw new Exception("Can't open WSDF file '{$wsdlFile}'");
		
		$this->xpath = new DOMXPath($this->dom);
		$this->xpath->registerNamespace("xsd", "http://www.w3.org/2001/XMLSchema");
		$this->xpath->registerNamespace("wsdl", "http://schemas.xmlsoap.org/wsdl/");
		
		if (!$this->portType)
		{
			// если не указан portType, то берем самый первый
			$portType = $this->xpath->query("//wsdl:definitions/wsdl:portType");
			$this->portType = $portType->item(0)->getAttribute("name");
			$this->log("portType was not defined. Using '{$this->portType}'");
		}	
	}
	
	/**
	 * Выполняет разбор типов данных и методов
	 * 
	 * @param string $portType
	 * 
	 * @return void
	 */
	public function parse($portType = null)
	{
		$this->parseDataTypes();
		$this->parseOperations($portType);
	}
	
	/**
	 * Добавляет тип в коллекцию
	 * 
	 * @param string $className
	 * @param mixed $type
	 * 
	 * @return void
	 */
	private function addType($className, $type)
	{
		if (!array_key_exists($className, $this->collector))
			$this->collector[$className] = $type;
		else
			$this->log("\tWarning!!! Datatype '{$className}' has duplicate. Skipped.");
	}
	
	/**
	 * Возвращает документацию элемента
	 * 
	 * @param DOMElement $type элемент
	 * 
	 * @return string
	 */
	private function getComments(DOMElement $type)
	{
		$classDoc = $this->xpath->query("xsd:annotation/xsd:documentation", $type);
		$res = "";
		
		foreach ($classDoc as $doc)
			$res .= trim($doc->nodeValue) . "\n\n";
		
		return preg_replace('/[\s]{2,}/', "\n\t * ", $res);
	}	
	
	/**
	 * Выполняет разбор Типов данных WSDL
	 * 
	 * @return void
	 */
	public function parseDataTypes()
	{
		$this->log("Start parsing data types...", true);
		
		// target namespace
		$tns = $this->xpath->query("//wsdl:definitions/@targetNamespace");
		$this->targetNamespace = $tns->item(0)->value;
		
		// Список всех простых типов
		$simpleTypes = $this->xpath->query("//wsdl:types/xsd:schema/xsd:simpleType");
		
		foreach ($simpleTypes as $type)
		{
			$className = $type->getAttribute("name");
			$this->addType($className,  $this->parseSimpleType($type));
		}
				
		// Список всех сложных типов
		$complexType = $this->xpath->query("//wsdl:types/xsd:schema/xsd:complexType");
		foreach ($complexType as $type)
		{
			$className = $type->getAttribute("name");
			$this->addType($className,  $this->parseComplexType($type));
			//$this->collector[$className] = $this->parseComplexType($type);
		}
		
		// Список всех элементов
		$elements = $this->xpath->query("//wsdl:types/xsd:schema/xsd:element");
		foreach($elements as $type)
		{			
			$className = $type->getAttribute("name");			
			$this->addType($className,  $this->parseElement($type));
		}
		
		// строим карту типов
		foreach ($this->collector as $key => $value)
			$this->rawTypesMap[$key] = $value->baseType;

		// определим классы, наследующие от примитивов
		foreach ($this->collector as $key => $value)
		{
			$final = $this->findFinalType($key);
			$this->collector[$key]->isPrimitive = $this->isPrimitive($final);
			$this->collector[$key]->className = $key;
		}
	
	}
	
	/**
	 * Возвращает имя типа данных, описанного в message
	 * 
	 * @param DOMNode $method
	 * @param string $direct input|output
	 * 
	 * @return string
	 */
	private function getMessageType(DOMNode $method, $direct)
	{
		$outNode = $this->xpath->query("./wsdl:{$direct}", $method);
		if ($outNode->length > 0)
		{
			// имя входящего сообщения
			$messageName = $this->stripNamespace($outNode->item(0)->getAttribute("message"));
			
			// ссылка на element, в котором определен тип данных этого сообщения
			$message = $this->xpath->query("//wsdl:definitions/wsdl:message[@name='{$messageName}']/wsdl:part/@element");
			if ($message->item(0) == null)
			{
				// Значит, имеем определение типа прямо в message
				$this->log("\tCreating type '{$messageName}' from message");
				
				$parts = $this->xpath->query("//wsdl:definitions/wsdl:message[@name='{$messageName}']/wsdl:part");
				
				$el = $this->getNewType();
				$el->className = $messageName;
				$el->isPrimitive = false;
				$el->isMessagePart = true;
				
				foreach ($parts as $part)
				{
					$attr = new stdClass();
					$attr->name = $part->getAttribute("name");
					$attr->type = $this->stripNamespace($part->getAttribute("type"));	
					$attr->maxOccurs = null;
					$attr->doc = null;
					$el->attributes[] = $attr;
				}
				
				$this->addType($messageName, $el);
				$this->log("\t...ok");
				return $messageName;
			}
			else 
			{
				return $this->stripNamespace($message->item(0)->value);
			}
		}
		return null;
	}
	
	/**
	 * Разбор методов, описанных в WSDL
	 * 
	 * @param string $portType Имя набора операций
	 * 
	 * @return array
	 */
	public function parseOperations($portType = null)
	{
		if ($portType == null)
			$portType = $this->portType;

		$this->log("Start parsing operations from portType '{$portType}'...", true);
		
		$operations = $this->xpath->query("//wsdl:definitions/wsdl:portType[@name='{$portType}']/wsdl:operation");
		if ($operations->length == 0)
			throw new Exception("Please, define portType");
		
		foreach ($operations as $method)
		{
			$out = new stdClass();			
			$name = $method->getAttribute("name");
			$this->log("parsing operation '{$name}'");
			
			$input = $this->getMessageType($method, "input");
			$output = $this->getMessageType($method, "output");
			
			$docNode = $this->xpath->query("./wsdl:documentation", $method);
			$txt = "";
			if ($docNode->length > 0)
			{				
				foreach ($docNode as $doc)
					$txt = $txt . " ". $doc->nodeValue;
			}
			
			$out->input = $this->findFinalType($input);
			$out->output = $this->findFinalType($output);
			$out->operationDoc = $txt;
						
			$out->inputPrimitive = $this->isPrimitive($this->findFinalType($input));
			$out->outputPrimitive = $this->isPrimitive($this->findFinalType($output));
			
			$this->methods[$name] = $out;
			$this->log("...ok");
		}
	}
	
	public function findFinalType($typeName)
	{
		$typeName = trim($typeName);
		if ($this->isPrimitive($typeName))
			return $typeName;

		if (!array_key_exists($typeName, $this->rawTypesMap))
		{
			$this->log("Undefined type '{$typeName}'. Try add it into primitiveTypes in config for ignore.");
			return $typeName;
		}
		
		$type = $this->rawTypesMap[$typeName];
		if (!$type)
			return $typeName;

		if (!$this->isPrimitive($type))
			$type = $this->findFinalType($type);

		return $type;
	}
	
	/**
	 * Тип в списке примитивных?
	 * 
	 * @param string $typeName имя типа
	 * 
	 * @return boolean
	 */
	public function isPrimitive($typeName)
	{
		if (!$typeName)
			return false;

		return in_array($typeName, $this->primitiveTypes);
	}
	

	/**
	 * Парсит простые типы
	 * 
	 * @param DomNode $type элемент
	 * 
	 * @return mixed
	 */
	private function parseSimpleType($type)
	{		
		$res = $this->getNewType();
		
		$className = $type->getAttribute("name");
		$this->log("parse {$className}");
		
		$baseType = $this->stripNamespace($this->xpath->query("xsd:restriction/@base", $type)->item(0)->value);

		$enum = $this->xpath->query("xsd:restriction/xsd:enumeration", $type);	
		$isEnum = $enum->length !== 0;
		
		$res->className = $className;
		$res->baseType = $baseType;
		$res->isEnum = $isEnum;
		$res->nodeType = "simpleType";
		$res->classDoc = $this->getComments($type);
		
		// парсим Enum
		if ($isEnum)
		{
			foreach ($enum as $item)
			{
				$value = $item->getAttribute("value");
				$doc = $this->getComments($item);
				
				$obj = new stdClass();
				$obj->value = $value;
				$obj->comment = $doc;
				$res->enumeration[] = $obj;
			}
		}

		$this->log("...ok");
		return $res;
	}
	
	/**
	 * Парсит поседовательности
	 * 
	 * @param DomNode $type Элемент
	 * @param string $externalClassName Имя класса ,в котором находится последовательность
	 * 
	 * @return mixed
	 */	
	private function parseSequence($sequence, $externalClassName)
	{
		$this->log("\tparse sequence in {$externalClassName}");
		$elements = $this->xpath->query("./xsd:element", $sequence);
		$res = array();
		foreach ($elements as $item)
		{
			$typeName = $item->getAttribute("type");
			$name = $item->getAttribute("name");
			$this->log("\t\t{$name} => {$typeName}");
			if ($typeName)
			{
				$obj = new stdClass();
				$obj->name = $name;
				$obj->maxOccurs = $item->getAttribute("maxOccurs");
				$obj->type = $this->stripNamespace($typeName);
				$obj->doc = $this->getComments($item);				
				$res[] = $obj;

			}
			else
			{		
				// у этого элемента нет атрибута type, поэтому считаем, что он является определением нового типа
				$newElement = $this->parseElement($item);
				$className = $externalClassName . ucfirst($name);
				$this->addType($className, $newElement);
				
				$el = new stdClass();
				$el->name = $name;
				$el->type = $className;
				$el->maxOccurs = $item->getAttribute("maxOccurs");
				$el->doc = $this->getComments($item);

				$res[] = $el;				
			}
		}

		$this->log("\t...ok");
		return $res;
	}
	
	
	/**
	 * Парсит элементы
	 * 
	 * @param DomNode $type Элемент
	 * 
	 * @return mixed
	 */	
	private function parseElement($type)
	{
		$className = $type->getAttribute("name");
		$this->log("parsing element {$className}");
			
		$res = $this->getNewType();
		
		$res->className = $className;
		$baseType = $this->stripNamespace($type->getAttribute("type"));
		$res->nodeType = "element";
		$res->classDoc = $this->getComments($type);

		// парсим структуру complexType->sequence
		$sequence = $this->xpath->query("./xsd:complexType/xsd:sequence", $type);		
		foreach ($sequence as $item)
			$res->attributes = $this->parseSequence($item, $className);

		// парсим структуру complexContent->sequence
		$complexContent = $this->xpath->query("./xsd:complexType/xsd:complexContent", $type);
		foreach($complexContent as $item)
		{
			$bt = $this->xpath->query("./xsd:extension/@base", $item);
			$baseType = $this->stripNamespace($bt->item(0)->value);
			
			$sequence = $this->xpath->query("./xsd:extension/xsd:sequence", $item);	
			foreach ($sequence as $seq)
				$res->attributes = $this->parseSequence($seq, $className);
		}

		$res->baseType = $baseType;

		// парсим все дочерние simpleType
		$simpleTypes = $this->xpath->query("./xsd:simpleType", $type);
		foreach ($simpleTypes as $item)
			$res = $this->parseSimpleType($item);
		
		$this->log("...ok");
		return $res;
	}
	
	/**
	 * Парсит комплексные типы
	 * 
	 * @param DomNode $type Элемент
	 * 
	 * @return mixed
	 */
	private function parseComplexType($type)
	{
		// по спецификации сomplexType может иметь более сложную структуру,
		// нежели разбирается в этом методе.		
		
		$res = $this->getNewType();
		
		$className = $type->getAttribute("name");
		$this->log("parse ComplexType {$className}");
		
		$res->classDoc = $this->getComments($type);
		$res->className = $className;
		//
		$res->nodeType = "complexType";
		
		// парсим структуру complexType->sequence
		$sequence = $this->xpath->query("./xsd:sequence", $type);
		foreach ($sequence as $item)
		{
			$res->attributes = $this->parseSequence($item, $className);
		}
		
		// парсим структуру complexContent->extension->sequence
		$complexContent = $this->xpath->query("./xsd:complexContent/xsd:extension/xsd:sequence", $type);
		foreach ($complexContent as $sequence)
		{
			$res->attributes = $this->parseSequence($sequence, $className);
		}
		
		// В описании complexType могут быть элементы <xsd:attribute/>
		$xsdAttributes = $this->xpath->query("./xsd:attribute", $type);
		
		foreach ($xsdAttributes as $attribute)
		{			
			$obj = new stdClass();
			$obj->name = $attribute->getAttribute("name");
			$obj->maxOccurs = null;
			$obj->type = $this->stripNamespace($attribute->getAttribute("type"));
			$obj->doc = $this->getComments($attribute);				
			$res->attributes[] = $obj;
			$this->log("\tadd attribute: {$obj->name} => {$obj->type}");
		}
		
		// Базовый тип
		$bt = $this->xpath->query("./xsd:complexContent/xsd:extension/@base", $type);
		if ($bt->item(0))
			$res->baseType = $this->stripNamespace($bt->item(0)->value);

		$this->log("...ok");
		return $res;
	}
	
	
	/**
	 * Пустая структура элемента
	 * 
	 * @return stdClass
	 */
	private function getNewType()
	{
		$obj = new stdClass();
		$obj->className = null; // Имя типа данных
		$obj->classDoc = null; // документация к типу
		$obj->baseType = null; // базовый тип для этого типа
		$obj->nodeType = null; // тип элемента: simpleType, complexType, element
		$obj->isEnum = false; // Это перечисление?
		$obj->enumeration = null;
		$obj->attributes = array(); // атрибуты типа
		$obj->isMessagePart = false; // тип декларирован в message?
		return $obj;
	}
	
	/**
	 * Убирает пространства имен из названий типов
	 * 
	 * @param string $attrType Строка, содержаящая тип
	 * 
	 * @return string
	 */
	private function stripNamespace($attrType)
	{
		$tmp = explode(":", $attrType);
		return isset($tmp[1]) ? $tmp[1] : $attrType;
	}
	
	/**
	 * Вывод отладочной информации в консоль
	 *
	 * @param string $message Text of message
	 * @param boolean $section Show header if True
	 * 
	 * @return void
	 */
	private function log($message, $section = false)
	{
		if (!$this->logEnabled)
			return false;
			
		if ($section)
		{
			$border = str_repeat("-", 100);
			$message = "\n{$border}\n{$message}\n{$border}\n";
		}
		echo "{$message}\n";
	}
}

?>